﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.SharePoint.Diagnostics;
using Microsoft.SharePoint.Administration;

namespace Behemoth.SharePoint.Logging {

  /// <summary>
  /// Implements a logging service that uses "Behemoth" as the product name.
  /// </summary>
  /// <remarks>
  /// Thanks to http://blog.mastykarz.nl/logging-uls-sharepoint-2010/ for the example!
  /// </remarks>
  public class BehemothLoggingService : SPDiagnosticsServiceBase {

    private const int DEFAULT_ID_TAG = 0;
    public const string SERVICE_NAME = "Behemoth Logging Service";
    public const string ULS_EXCEPTION_PAD = "###EXCEPTION###";

    // TODO implement a collection of providers and allow different modules to register their service in it and swithc the current provider.

    private static BehemothLoggingService currentProvider;
    public static BehemothLoggingService Current {
      get {
        if (currentProvider == null) {
          currentProvider = new BehemothLoggingService();
        }
        return currentProvider;
      }
    }

    private static BehemothLoggingService defaultProvider;
    public static BehemothLoggingService Default {
      get {
        if (defaultProvider == null) {
          defaultProvider = new BehemothLoggingService();
        }
        return defaultProvider;
      }
    }

    private string assemblyName;
    public string AssemblyName {
      get {
        if (string.IsNullOrEmpty(assemblyName)) {
          assemblyName = System.Reflection.Assembly.GetCallingAssembly().FullName;
          int pos = assemblyName.IndexOf(',');
          if (pos >= 0)
            assemblyName = assemblyName.Substring(0, pos);
        }
        return assemblyName;
      }
    }

    // reserved for future use
    public string ExeName {
      get {
        return string.Empty;
      }
    }

    private TraceSeverity defaultTrace = (TraceSeverity)Enum.Parse(typeof(TraceSeverity), "Medium", true); // TODO get me from somewhere
    public TraceSeverity DefaultTrace {
      get {
        return defaultTrace;
      }
      set {
        defaultTrace = value;
      }
    }
    private EventSeverity defaultEvent = (EventSeverity)Enum.Parse(typeof(EventSeverity), "Information", true); // TODO get me from somewhere
    public EventSeverity DefaultEvent {
      get {
        return defaultEvent;
      }
      set {
        defaultEvent = value;
      }
    }

    private string defaultProduct = "Behemoth"; // TODO get me from somewhere
    public string DefaultProduct {
      get {
        return defaultProduct;
      }
      set {
        defaultProduct = value;
      }
    }

    private string defaultCategory = "DefaultCategory"; // TODO get me from somewhere
    public string DefaultCategory {
      get {
        return defaultCategory;
      }
      set {
        defaultCategory = value;
      }
    }

    // TODO for whatever reason that this was a private constructor...
    public BehemothLoggingService()
      : base(SERVICE_NAME, SPFarm.Local) {
    }

    private List<SPDiagnosticsCategory> categories = new List<SPDiagnosticsCategory>();
    private List<SPDiagnosticsArea> areas = new List<SPDiagnosticsArea>();
    private SPDiagnosticsCategory EnsureCategory(string categoryName) {
      foreach (SPDiagnosticsCategory cat in categories) {
        if (categoryName == cat.Name)
          return cat;
      }
      SPDiagnosticsCategory newCat = new SPDiagnosticsCategory(
        categoryName,
        DefaultTrace,
        DefaultEvent
      );
      categories.Add(newCat);
      UpdateAreas();
      return newCat;
    }

    protected virtual void UpdateAreas() {
      for (int i = 0; i < areas.Count; i++) {
        //SPDiagnosticsArea area = areas[i];
        //TODO it seems impossible, but some logic to only do this when the values actually change would be great
        //for all I know, this entire approach is flawed and will not work.
        SPDiagnosticsArea newArea = new SPDiagnosticsArea(
          DefaultProduct,
          categories
        );
        areas[i] = newArea;
      }
    }

    protected virtual void EnsureAreas() {
      EnsureCategory(DefaultCategory);
      if (areas == null || areas.Count < 1) {
        SPDiagnosticsArea area = new SPDiagnosticsArea(
          DefaultProduct,
          categories
        );
        areas.Add(area);
      }
    }
    protected virtual void EnsureAreas(string areaName, string categoryName) {
      EnsureCategory(categoryName);
      SPDiagnosticsArea area;
      try {
        area = this.Areas[areaName];
      } catch {
        area = new SPDiagnosticsArea(
          areaName,
          categories
        );
        areas.Add(area);
      }
    }

    protected override IEnumerable<SPDiagnosticsArea> ProvideAreas() {
      EnsureAreas();
      return areas;
    }

    private SPDiagnosticsCategory GetCategory(string productName, string categoryName) {
      EnsureAreas(productName, categoryName);
      SPDiagnosticsCategory category = null;
      try {
        SPDiagnosticsArea area = Areas[productName];
        category = area.Categories[categoryName];
      } catch { }
      if (category == null && categoryName != this.DefaultCategory) {
        category = GetCategory(productName, this.DefaultCategory); // note that a double failure will return null
        if (category == null)
          throw new ArgumentNullException(
            string.Format(
              "Logging engine failed to get category '{0}' (or default '{1}') from product/area '{2}'.",
              categoryName,
              this.DefaultCategory,
              productName), "category");
      }
      return category; 
    }

    #region Log Writing Logic

    // TODO what ever happened to 'correlation' and 'assembly', are they automatic now

    public void Write(string message) {
      SPDiagnosticsCategory category = GetCategory(this.DefaultProduct, this.DefaultCategory);
      WriteTrace(DEFAULT_ID_TAG, category, category.DefaultTraceSeverity, message);
    }
    public void Write(string message, TraceSeverity severity) {
      Write(message, severity, this.DefaultProduct, this.DefaultCategory);
    }
    public void Write(string message, TraceSeverity severity, string productName) {
      Write(message, severity, productName, this.DefaultCategory);
    }
    public void Write(string message, TraceSeverity severity, string productName, string categoryName) {
      SPDiagnosticsCategory category = GetCategory(productName, categoryName);
      WriteTrace(DEFAULT_ID_TAG, category, severity, message);
    }

    public void Write(Exception ex) {
      Write(ex, this.DefaultProduct, this.DefaultCategory);
    }
    public void Write(Exception ex, string productName) {
      Write(ex, productName, this.DefaultCategory);
    }
    public void Write(Exception ex, string productName, string categoryName) {
      string message = BuildErrorMessage(ex);
      TraceSeverity severity = TraceSeverity.Unexpected;
      SPDiagnosticsCategory category = GetCategory(productName, categoryName);
      WriteTrace(DEFAULT_ID_TAG, category, severity, message);
    }

    protected string BuildErrorMessage(Exception ex) {
      return BuildErrorMessage(ex, true);
    }
    protected string BuildErrorMessage(Exception ex, bool usePadHeader) {
      if (ex == null)
        return string.Empty;
      string pad = usePadHeader ? ULS_EXCEPTION_PAD + " " : string.Empty;
      string exType = ex.GetType().FullName;
      string exTypeAndSrc = string.IsNullOrEmpty(ex.Source) ? exType : string.Format("{0} in {1}", exType, ex.Source);
      string errorText = string.Format("{0}{1}: {2} {3}", pad, exTypeAndSrc, ex.Message, ex.StackTrace);
      if (ex.InnerException != null)
        errorText += BuildErrorMessage(ex.InnerException, false);
      return errorText;
    }

    #endregion

    public void Log(object sender, LoggingEventArgs e) {
      if (e.Exception != null)
        Write(e.Exception, e.Product, e.Category);
      else
        Write(e.Message, e.Severity, e.Product, e.Category);
    }

  }

  // see also http://www.sharepointoverflow.com/questions/2057/how-to-write-to-uls-log-in-sharepoint-2010-from-a-net-app

}
